using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace PhpDocBlockGenerator.Model.Php
{
    /// <summary>
    /// php file class to extract doc block informations from
    /// </summary>
    public class PhpFile
    {
        /// <summary>
        /// the whole file in a generic list
        /// </summary>
        private List<String> _fileArr;

        /// <summary>
        /// the whole file in a generic list -> unedited
        /// </summary>
        private List<String> _fileArrUntouched;

        /// <summary>
        /// the fileinfo for that file
        /// </summary>
        private FileInfo _fileInfo;

        /// <summary>
        /// the path to the file, that should be read
        /// </summary>
        private String _filePath;

        /// <summary>
        /// the lines, that shouldn't be parsed
        /// </summary>
        private List<int> _exceptedLines = new List<int>();

        /// <summary>
        /// the file-docblock generated by the program itself
        /// </summary>
        private PhpFileDocBlock _generatedDocBlock;

        /// <summary>
        /// the docblock merged from the user and the generated one
        /// </summary>
        private PhpFileDocBlock _mergedDocBlock;

        /// <summary>
        /// the path where the new file should be written
        /// </summary>
        private String _newFilePath;

        /// <summary>
        /// the file-docblock written by the user
        /// </summary>
        private PhpFileDocBlock _userDocBlock;

        /// <summary>
        /// All Classes within a file
        /// </summary>
        private List<PhpClass> _phpClasses = new List<PhpClass>();

        /// <summary>
        /// All Parent Paths of the File
        /// </summary>
        private List<String> _prePaths;

        /// <summary>
        /// the options container
        /// </summary>
        private Options _options;

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="filePath">file name and path to read the file</param>
        /// <param name="newFilePath">file name and path to save the file</param>
        /// <param name="fileInfo"></param>
        /// <param name="prePaths"></param>
        /// <param name="options"></param>
        public PhpFile(String filePath, String newFilePath, FileInfo fileInfo, List<String> prePaths, Options options)
        {
            _fileInfo = fileInfo;
            _filePath = filePath;
            _newFilePath = newFilePath;
            _prePaths = prePaths;
            _options = options;
        }

        /// <summary>
        /// execute all operations to fill the object
        /// </summary>
        public void Execute()
        {
            try
            {
                try
                {
                    // read file
                    _ReadFileToBuffer(_filePath);

                    // parse the file docblock
                    _ParseFileDocBlock();

                    //generate own file doc block
                    _GenerateDocBlock();

                    // get commented lines
                    _GetExceptionList();

                    // parse file for classes
                    _ParseFile();

                    // Compare and Merge the several docblocks
                    _CompareAndMerge();
                }
                catch (Exception)
                {
                    //add files to list of corrupted or invalid files
                    _options.AddFileToFilesWithErrors(_filePath);

                    //don't re-throw exception here to avoid adding file to error list twice
                }

                //TODO: catch exception for writing file (or do it one catch-level higher)

                // write the new file
                _WriteFile(_newFilePath);
            }
            catch (Exception)
            {
                //add files to list of corrupted or invalid files
                _options.AddFileToFilesWithErrors(_filePath);
            }
            finally
            {
                //update progress bar in any case
                _options.UpdateProgressWithOneFile();
            }
        }


        /// <summary>
        /// checks whether the the docblock above a class could be a filedocblock
        /// </summary>
        private Boolean _CheckForFileDocBlockAttributes(List<String> fileArr)
        {
            //loop each line to find the propertykeywords
            return fileArr.Select(t => PhpRegex.RegSearchPropAuthorOrVersion.Matches(t)).Any(regFileDocBlockPropertiesResult => regFileDocBlockPropertiesResult.Count > 0);
        }

        /// <summary>
        /// compares and merges the generated file doc block to the existing one,
        /// calls also the compare methods of all classes, merges them
        /// and writes the merged data back to the list of file lines
        /// </summary>
        private void _CompareAndMerge()
        {
            //merge file doc blocks
            _CompareAndMergeFileDocBlocks();

            //loop over all classes and merge data
            foreach (PhpClass c in _phpClasses)
                c.CompareAndMerge(_mergedDocBlock);
        }

        /// <summary>
        /// compares and merges the generated and the read doc block
        /// </summary>
        private void _CompareAndMergeFileDocBlocks()
        {
            //if read doc block exists
            _mergedDocBlock = _userDocBlock != null ? _userDocBlock.Compare(_generatedDocBlock) : _generatedDocBlock;
        }

        /// <summary>
        /// All commented Lines shouldnt be parsed, so search for them and
        /// add them to a list
        /// </summary>
        private void _GetExceptionList()
        {
            Boolean activeComment = false;
            
            //loop over all file lines
            for (int i = 0; i < _fileArr.Count; ++i)
            {
                if (!activeComment)
                {
                    //search for "/*"
                    MatchCollection regBeginOfComment = PhpRegex.RegSearchBeginOfComment.Matches(_fileArr[i]);

                    //if it was found
                    if (regBeginOfComment.Count > 0)
                    {
                        activeComment = true;
                        _exceptedLines.Add(i);
                    }
                }

                if (activeComment)
                {
                    //search for "*/"
                    MatchCollection regEndOfComment = PhpRegex.RegSearchEndOfDocBlock.Matches(_fileArr[i]);

                    //if it was found
                    if (regEndOfComment.Count > 0)
                    {
                        activeComment = false;
                    }
                    if (!_exceptedLines.Contains(i))
                        _exceptedLines.Add(i);
                }
            }
        }

        /// <summary>
        /// Generates the DocBlock
        /// </summary>
        public void _GenerateDocBlock()
        {
            var fdb = new PhpFileDocBlock(_options)
                {
                    Package = new DocBlockProperty(_options)
                        {
                            Description = String.Join("\\", _prePaths),
                            ObjectType = DocBlockPropertyTypes.Package
                        },
                    Copyright = new DocBlockProperty(_options)
                        {
                            Description =
                                "Copyright (c) " + _fileInfo.CreationTime.Year.ToString(CultureInfo.InvariantCulture) +
                                " Unister GmbH",
                            ObjectType = DocBlockPropertyTypes.Copyright
                        }
                };


            fdb.Author.Add(new DocBlockProperty(_options)
                {
                    Description = DocBlockProperty.AuthorUnisterText,
                    ObjectType = DocBlockPropertyTypes.Author
                }
            );

            fdb.Author.Add(new DocBlockProperty(_options)
                {
                    Description = DocBlockProperty.AuthorStandardText,
                    ObjectType = DocBlockPropertyTypes.Author
                }
            );

            fdb.Version = new DocBlockProperty(_options)
            {
                Description = DocBlockProperty.VersionStandardText,
                ObjectType = DocBlockPropertyTypes.Version
            };

            fdb.ShortDescription.Description = "<Description of " + _fileInfo.Name + ">";

            _generatedDocBlock = fdb;
        }

        /// <summary>
        /// parses the file for classes, methods and their doc blocks
        /// </summary>
        private void _ParseFile()
        {
            //Parse for Classes
            var classMatches = new List<Match>();
            var classMatchesBeginPositions = new List<int>();
            var classMatchesGreedyBeginPositions = new List<int>();


            //loop over all file lines
            for (int i=0;i<_fileArr.Count;++i)
            {
                //skip excepted lines
                if (!_exceptedLines.Contains(i))
                {
                    //search for "class" in current line
                    MatchCollection regClassNameResult = PhpRegex.RegSearchClassName.Matches(_fileArr[i]);

                    //if it was found
                    if (regClassNameResult.Count > 0)
                    {
                        //reset position of the closing curly brace to zero in case of
                        //no class exists upwards
                        int upwardsClosingCurlyBracePos = 0;

                        //loop over all lines over the match
                        for (int j = i; j >= 0; --j)
                        {
                            //search for closing curly brace of the previous class (if exists) in the current line
                            MatchCollection regClosingCurlyBraceResult = PhpRegex.RegSearchClosingCurlyBrace.Matches(_fileArr[j]);

                            //if brace was found
                            if (regClosingCurlyBraceResult.Count > 0)
                            {
                                //save position and exit loop
                                upwardsClosingCurlyBracePos = j + 1;
                                break;
                            }
                        }

                        //save beginning position in list to extract file lines for current class
                        //later and also save match data
                        classMatches.Add(regClassNameResult[0]);
                        classMatchesBeginPositions.Add(i);
                        classMatchesGreedyBeginPositions.Add(upwardsClosingCurlyBracePos);
                    }
                }
            }

            //create a list with all classes
            var tempClassList = new List<PhpClass>();

            //loop over all class matches
            for (int i = 0; i < classMatches.Count; ++i)
            {
                int begin = classMatchesGreedyBeginPositions[i];
                int length;


                //if last class in file is reached
                if (i == classMatches.Count - 1)
                {
                    //set the length to file lines from previous class-end to end of file
                    length = _fileArr.Count - classMatchesGreedyBeginPositions[i] -1;
                }
                else
                {
                    //set the length to file lines from previous class-end until next class is reached
                    length = classMatchesGreedyBeginPositions[i + 1] - classMatchesGreedyBeginPositions[i] -1;   
                }

                //now extract
                List<string> classLines = _fileArr.GetRange(begin, length + 1);

                //add class object to list
                var tempClass = new PhpClass(classLines, _options)
                    {
                        ClassName = classMatches[i].Value,
                        BeginningLine = classMatchesBeginPositions[i],
                        Endline = begin + length
                    };
                tempClassList.Add(tempClass);
            }

            //iterate the temporary class list, search for class-docblocks and remove
            //lines, which dont belong to the several classes
            //at the end only classes and the corresponding docblock should be written
            //into the file array
            for (int i = 0; i < tempClassList.Count;++i )
            {
                //search for a docblock above of the class
                int docBegin, docEnd;
                StaticMethods.ParseDocBlockPos(_fileArr, tempClassList[i].BeginningLine, out docBegin, out docEnd);

                //get the beginning-line and calculate the length of the class
                int beginningLine = tempClassList[i].BeginningLine;
                int length = tempClassList[i].Endline - tempClassList[i].BeginningLine + 1;

                //if docblock was found and it is the first class in the file, 
                //check whether it could be a file docblock
                if (-1 != docBegin && -1 != docEnd && 0 == i)
                {
                    List<String> tempDocBlock = _fileArr.GetRange(docBegin, docEnd - docBegin + 1);

                    //if the found docblock isn't the same as the file docblock
                    //set the first line of the class to the beginning-line of the docblock
                    if (_userDocBlock == null || docBegin != _userDocBlock.DocBlockBegin)
                    {
                        int lengthAdd = beginningLine - docBegin;
                        beginningLine = docBegin;
                        length += lengthAdd;

                        //add user doc block to class and parse its content
                        tempClassList[i].UserDocBlock = new PhpClassDocBlock(_options)
                            {
                                DocBlockBegin = docBegin,
                                DocBlockEnd = docEnd,
                                IsUserBlock = true
                            };
                        tempClassList[i].UserDocBlock.Parse(tempDocBlock);

                        //add the doc block length to get the correct location of
                        //the class keyword in the local list of file lines of the class
                        tempClassList[i].BeginningLineClassKeywordExtractedPart += lengthAdd;
                    }
                }

                //extract the file lines of the class
                tempClassList[i].FileArr = _fileArr.GetRange(beginningLine, length);

                //save new beginning of the class
                tempClassList[i].BeginningLine = beginningLine;

                //save the adapted exception-line-list
                tempClassList[i].ExceptedLines = StaticMethods.AdaptExceptedLines(_exceptedLines, beginningLine);

                //lazy-loading is active, so parse methods and class doc block
                tempClassList[i].ReadIn();

                //add the class object to the internal phpclasses-object-stack of this class
                _phpClasses.Add(tempClassList[i]);
            }
        }
        
        /// <summary>
        /// checks if a filedocblock exists and parses it into
        /// a filedocblock-object
        /// </summary>
        public void _ParseFileDocBlock()
        {
            //flag to determine whether the beginning of the docblock has been found in the file
            //content
            Boolean activeDocBlock = false;

            //flag to determine whether the php start tag has been found in the file content
            Boolean activePhpFile = false;

            // Create a new, temporarily filedocblock-object
            var tempFdb = new PhpFileDocBlock(_options);

            // Search for a Beginning of a Docblock
            for (int i = 0; i < _fileArr.Count; ++i)
            {
                //if the php-starttag was found
                if (activePhpFile)
                {
                    // if the beginning of the docblock wasnt found yet
                    if (!activeDocBlock)
                    {
                        // if DocBlock was found
                        if (PhpRegex.RegSearchBeginOfDocBlock.IsMatch(_fileArr[i]))
                        {
                            activeDocBlock = true;

                            tempFdb.DocBlockBegin = i;
                        }
                        // if no DocBlock was found, check for other Characters
                        else
                        {
                            // if other characters than whitespaces are found, no docblock exists
                            if (PhpRegex.RegSearchForAnyCharacter.IsMatch(_fileArr[i]))
                            {
                                break;
                            }
                        }
                    }
                    // if the beginning of a docblock was found, search for the end
                    else
                    {
                        if (PhpRegex.RegSearchEndOfDocBlock.IsMatch(_fileArr[i]))
                        {
                            tempFdb.DocBlockEnd = i;
                            List<String> tempDocBlockLines = _fileArr.GetRange(
                                tempFdb.DocBlockBegin, tempFdb.DocBlockEnd - tempFdb.DocBlockBegin + 1);
                            if(_CheckForFileDocBlockAttributes(tempDocBlockLines))
                            {
                                tempFdb.Parse(tempDocBlockLines);
                                _userDocBlock = tempFdb;
                            }
                            break;
                        }
                    }
                }
                else
                {
                    //php start tag hasn't been found as yet -> search it in current file line
                    //and set flag when found
                    if (PhpRegex.RegSearchForPhpStartTag.IsMatch(_fileArr[i]))
                    {
                        activePhpFile = true;
                    }
                }
            }
        }

        /// <summary>
        /// Reads file into memory
        /// </summary>
        /// <param name="filePath"></param>
        private void _ReadFileToBuffer(String filePath)
        {
            //open file to read and close automatically at the end of the scope
            using (StreamReader sr = File.OpenText(filePath))
            {
                var fileArr = new List<String>();

                //read all lines of the file to a list
                while (!sr.EndOfStream)
                    fileArr.Add(sr.ReadLine());

                _fileArr = fileArr;
                _fileArrUntouched = new List<String>(fileArr);
            }
        }

        /// <summary>
        /// deletes tabs and makes intelligent indents
        /// </summary>
        /// <param name="lineNumber"></param>
        /// <param name="newFileArr"></param>
        /// <returns></returns>
        private String _ReworkIndent(int lineNumber, List<String> newFileArr)
        {
            // delete tabs and insert 4 whitespaces
            String returnString = PhpRegex.RegTabs.Replace(newFileArr[lineNumber], "    ");

            // Search for "->" at the Beginning
            MatchCollection regObjectOperatorAtBeginning = PhpRegex.RegObjectOperatorAtBeginning.Matches(returnString);

            //if found
            if (regObjectOperatorAtBeginning.Count > 0)
            {
                //Search for "->" at the whole Line
                MatchCollection regObjectOperator;
                int i = 1;
                do
                {
                    String lineAbove = PhpRegex.RegTabs.Replace(newFileArr[lineNumber - i], "    ");
                    regObjectOperator = PhpRegex.RegObjectOperator.Matches(lineAbove);
                    --i;
                } while (regObjectOperator.Count < 1 && lineNumber - i != 0);

                if (regObjectOperator.Count > 0)
                {
                    String spaces = String.Empty;
                    int spaceNumbers = regObjectOperator[0].Index;
                    returnString = returnString.Trim();
                    for (int j = 0; j < spaceNumbers; ++j)
                    {
                        spaces += " ";
                    }
                    returnString = spaces + returnString;
                }
            }
            return returnString;
        }

        /// <summary>
        /// writes the merged doc block to the list of file lines and removes the old one
        /// </summary>
        private void _WriteDocBlock(ref int writingOffset)
        {
            //TODO: need own exception type here
            //if no merged doc block exists then throw exception
            if (null == _mergedDocBlock)
                throw new Exception();


            //get doc block as list of lines
            List<String> docBlockAsList = _mergedDocBlock.ToList();


            //calc number of lines of old doc block (if exists) and remove it from list
            //of file lines (and decrease offset by the number of removed lines)
            if (null != _userDocBlock &&
                -1 != _userDocBlock.DocBlockBegin && -1 != _userDocBlock.DocBlockEnd)
            {
                int numberOfLinesOldBlock =
                    _userDocBlock.DocBlockEnd - _userDocBlock.DocBlockBegin + 1;

                _fileArr.RemoveRange(_userDocBlock.DocBlockBegin, numberOfLinesOldBlock);
                writingOffset -= numberOfLinesOldBlock;
            }


            //search for php starting tag on first line
            MatchCollection regSearchPhpStartTag = PhpRegex.RegSearchForPhpStartTag.Matches(_fileArr[0]);

            int startline;

            //if there is no starting tag then surround doc block with
            //start tag and end tag and set writing position for doc block to first line
            if (regSearchPhpStartTag.Count == 0)
            {
                startline = 0;

                docBlockAsList.Add("?>");
                docBlockAsList.Insert(0, "<?php");
            }
            //otherwise there is an existing php start tag so set writing
            //position for doc block to second line
            else
            {
                startline = 1;
            }

            //write doc block to list of file lines and increase offset by
            //the number of added lines
            _fileArr.InsertRange(startline, docBlockAsList);
            writingOffset += docBlockAsList.Count;
        }

        /// <summary>
        /// writes the file back to harddrive in a new project path for all
        /// merged files
        /// </summary>
        /// <param name="newFilePath"></param>
        private void _WriteFile(String newFilePath)
        {
            _CreateFolderIfNotExists(newFilePath);

            //get the complete path for the file
            String filePath = Path.Combine(newFilePath, _fileInfo.Name);

            //create file if not exists
            if(!File.Exists(filePath))
            {
                FileStream fs = File.Create(filePath);
                fs.Close();
            }

            // Make an Offset
            int offset = 0;

            //write doc block to list of lines and remove old one
            _WriteDocBlock(ref offset);

            //loop over all classes and write their new data to list of file lines
            foreach (PhpClass c in _phpClasses)
            {
                c.WriteToListOfFileLines(_fileArr, ref offset);
            }

            // Check the answer for parse-errors
            PhpErrorCheck pec = null;
            if(_options.ValidateWithPhp)
                pec = new PhpErrorCheck(_options.PhpPath, filePath);

            //check uncorrectness of php file
            if (pec != null && (_options.ValidateWithPhp && !pec.IsValid()))
            {
                _options.AddFileToFilesWithErrors(filePath);

                //create and open new file to write and close it at the end of the scope
                using (StreamWriter sw = File.CreateText(filePath))
                {
                    //loop over all lines and write them to file
                    foreach (String line in _fileArrUntouched)
                        sw.Write(line + "\n");

                    // close the stream-writer
                    sw.Close();
                }
            }
            //otherwise file is valid
            else
            {
                var newArray = new List<String>();


                //create and open new file to write and close it at the end of the scope
                using (StreamWriter sw = File.CreateText(filePath))
                {
                    //loop over all lines and write them to file
                    for (int i = 0; i < _fileArr.Count; ++i)
                    {
                        newArray.Add(_fileArr[i]);
                        String lineWithoutTabs = _ReworkIndent(i, newArray);
                        newArray.RemoveAt(i);
                        newArray.Add(lineWithoutTabs);
                        sw.Write(lineWithoutTabs + "\n");
                    }

                    // close the stream-writer
                    sw.Close();
                }
            }
        }

        /// <summary>
        /// creates the folder if it did not exist
        /// </summary>
        /// <param name="newFilePath"></param>
        private void _CreateFolderIfNotExists(string newFilePath)
        {
            if (!Directory.Exists(newFilePath))
                Directory.CreateDirectory(newFilePath);
        }
    }
}
